package edu.gatech.ors.mrsim.io.conf;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.IllegalFormatException;
import java.util.InvalidPropertiesFormatException;
import java.util.Properties;

import org.apache.log4j.Logger;

import edu.gatech.ors.mrsim.exceptions.WrongClassTypeException;
import edu.gatech.ors.mrsim.helpers.StringUtil;
import edu.gatech.ors.mrsim.io.logging.Logging;

/**
 * This is a simple representation of a generic configuration of a module or the
 * entire simulator. As in Hadoop, it stores key-value pairs with the keys being
 * strings and the values being ints, boolean, strings, or other types. It
 * extends Java's Properties class and so can store in XML as well as plaintext
 * files. A default configuration file is specified, though this supports other
 * files as well. This class can also be used for temporary runtime
 * configuration that is never saved to disk.
 * 
 * @author Andrey Kurenkov (akurenkov3@gatech.edu)
 * @version 1.0
 */
public class Configuration extends Properties {
    private File propertyFile;
    private boolean isXML;
    private static final String PATH_TO_DEFAULT = "conf" + File.separator
	    + "default_conf.xml";
    private static final String PATH_TO_TEST = "conf" + File.separator
	    + "test_conf.xml";
    private static final String PATH_TO_DEBUG = "conf" + File.separator
	    + "debug_conf.xml";
    private static Configuration globalConfig;
    private static final File DEFAULT_FILE = new File(PATH_TO_DEFAULT);
    private static final File TEST_FILE = new File(PATH_TO_TEST);
    private static final File DEBUG_FILE = new File(PATH_TO_DEBUG);

    private final static BasicLoader BASIC_CLASS_LOADER = new BasicLoader();
    public final static String LIST_SEPERATOR = ",";

    private final static Logger logger = Logging.getInstance().getClassLogger(
	    Configuration.class);

    /**
     * Plain constructor that creates a configuration not attached to any file
     * on the filesystem and containing no values.
     */
    public Configuration() {
	super();
    }

    /**
     * Simple constructor that creates an empty set of configuration not
     * associated with any file. If any keys are retrieved that have not been
     * entered into this config, it will search defaultVals and return either a
     * value inside that or null.
     * 
     * @param defaultVals
     *            the default values to use for retrieval
     */
    public Configuration(Configuration defaultVals) {
	super(defaultVals);
    }

    /**
     * More general constructor for existing configuration file. The code that
     * supplies this file is expected to handle exceptions with it not existing
     * or not actually being a configuration file (as in, saved from
     * Configuration itself previously).
     * 
     * @param loadFrom
     *            the file to load the Configuration from. Must be the result of
     *            calling the write method of Configuration previously or
     *            already formatted correctly for Java's Properties class.
     * @throws InvalidPropertiesFormatException
     *             See the JDK Properties class
     * @throws FileNotFoundException
     * @throws IOException
     */
    public Configuration(File loadFrom)
	    throws InvalidPropertiesFormatException, FileNotFoundException,
	    IOException {
	propertyFile = loadFrom;
	isXML = loadFrom.getName().endsWith(".xml");
	if (loadFrom.exists()) {
	    if (isXML)
		loadFromXML(new FileInputStream(loadFrom));
	    else
		load(new FileInputStream(loadFrom));
	} else {
	    if (isXML)
		storeToXML(new FileOutputStream(loadFrom),
			"Configuration created with file " + loadFrom.getName()
				+ " at time " + StringUtil.getFormattedTime());
	    else
		store(new FileOutputStream(loadFrom),
			"Configuration created with file " + loadFrom.getName()
				+ " at time " + StringUtil.getFormattedTime());
	}
    }

    /**
     * Static getter/intializer for a configuration based on a hardcoded default
     * file. Since this file is specified and constant, it is expected to exist
     * and programmers using it should generally not handle the case where there
     * are exceptions(unlike the public constructor). Problems with this file
     * are thus logged but not generally expected and so not thrown.
     * 
     * @param create
     *            whether to create the file if it does not exist
     */
    public static Configuration loadDefault(boolean create) {
	return loadFromFile(DEFAULT_FILE, create);
    }

    /**
     * Static getter/intializer for a configuration based on a hardcoded test
     * file. Since this file is specified and constant, it is expected to exist
     * and programmers using it should generally not handle the case where there
     * are exceptions(unlike the public constructor). Problems with this file
     * are thus logged but not generally expected and so not thrown.
     * 
     * @param create
     *            whether to create the file if it does not exist
     */
    public static Configuration loadTest(boolean create) {
	return loadFromFile(TEST_FILE, create);
    }

    /**
     * Static getter/intializer for a configuration based on a hardcoded debug
     * file. Since this file is specified and constant, it is expected to exist
     * and programmers using it should generally not handle the case where there
     * are exceptions(unlike the public constructor). Problems with this file
     * are thus logged but not generally expected and so not thrown.
     * 
     * @param create
     *            whether to create the file if it does not exist
     */
    public static Configuration loadDebug(boolean create) {
	return loadFromFile(DEBUG_FILE, create);
    }

    /**
     * Helper method to load from Files while avoiding possible exceptions
     * thrown by using constructor.
     * 
     * @param load
     *            the file to load from
     * @param create
     *            whether to create the file if it does not exist
     */
    public static Configuration loadFromFile(File load, boolean create) {
	if (!load.exists())
	    if (!create)
		return null;
	try {
	    Configuration config = null;
	    if (!load.exists()) {
		boolean created = load.getParentFile().exists();
		if (!created) {
		    created = load.getParentFile().mkdirs();
		    if (!created)
			return null;
		}
		created = load.createNewFile();
		if (!created)
		    return null;

		config = new Configuration();
		if (load.getName().endsWith(".xml")) {
		    config.storeToXML(
			    new FileOutputStream(load),
			    "Created new config at path "
				    + load.getAbsolutePath() + " on "
				    + StringUtil.getFormattedTime());
		}
	    } else
		config = new Configuration(load);

	    return config;
	} catch (InvalidPropertiesFormatException e) {
	    logger.error(StringUtil.getStackTrace(e));
	} catch (IOException e) {
	    logger.error(StringUtil.getStackTrace(e));
	}
	return null;// if exception
    }

    /**
     * Sets a new static global configuration, as long as the configuration is
     * not null
     * 
     * @param newGlobal
     *            the new global Configuration
     * @throws NullPointerException
     *             is the given Configuration is null
     */
    public static void setGlobalConfig(Configuration newGlobal)
	    throws NullPointerException {
	if (newGlobal != null)
	    globalConfig = newGlobal;
	else
	    throw new NullPointerException(
		    "Trying to set a null global configuration is not allowed");
    }

    /**
     * Retrieves the global configuration. If it has not been set yet, it loads
     * the default configuration file and sets that to be global before
     * returning it.
     * 
     * @return the static global configuration
     */
    public static Configuration getGlobalConfig() {
	if (globalConfig == null)
	    globalConfig = Configuration.loadDefault(true);
	return globalConfig;
    }

    /**
     * Retrieves a boolean value associated with the given key. Boolean values
     * are stored with the toString of the boolean class and parsed by that
     * class. If the key is not found this returns null (thus the Boolean return
     * type).
     * 
     * @param key
     *            the key to retrieve the value for
     * @param defaultValue
     *            the default value to return
     * @return
     */
    public Boolean getBoolean(String key) {
	return (containsKey(key) ? Boolean.parseBoolean(getProperty(key))
		: null);
    }

    /**
     * Utility method to get property parsed to a list of booleans.
     * 
     * @param key
     *            key to get values for
     * @return the boolean values if right format and key contained, null
     *         otherwise
     */
    public boolean[] getBooleans(String key) {
	if (!containsKey(key))
	    return null;
	else {
	    try {
		String[] strings = getStrings(key);
		boolean[] booleans = new boolean[strings.length];
		for (int i = 0; i < booleans.length; i++) {
		    try {
			booleans[i] = Boolean.parseBoolean(strings[i]);
		    } catch (IllegalFormatException e) {
			return null;
		    }
		}
		return booleans;
	    } catch (Exception e) {
		logger.error(StringUtil.getStackTrace(e));
		return null;
	    }
	}
    }

    /**
     * Loads a class from one key in the configuration.
     * 
     * @param key
     *            the key of the class
     * @return The succesfully loaded class, or null on error.
     * @throws ClassNotFoundException
     */
    public Class<?> getClass(String key) throws ClassNotFoundException {
	return getClass(key, BASIC_CLASS_LOADER);
    }

    /**
     * Loads a class from one key in the configuration.
     * 
     * @param key
     *            the key of the class
     * @param loader
     *            the method of loading to use with the class
     * @return The succesfully loaded class, or null on error.
     * @throws ClassNotFoundException
     */
    public Class<?> getClass(String key, ConfigurationClassLoader loader)
	    throws ClassNotFoundException {
	return loader.loadClass(key, this);
    }

    /**
     * Loads a list of classes from one key in the configuration.
     * 
     * @param key
     *            the key of the list of classes
     * @return The succesfully loaded classes, or null on error.
     * @throws ClassNotFoundException
     */
    public Class<?>[] getClasses(String key) throws ClassNotFoundException {
	return getClasses(key, BASIC_CLASS_LOADER);
    }

    /**
     * Loads a list of classes from one key in the configuration.
     * 
     * @param key
     *            the key of the list of classes
     * @param loader
     *            the method of loading to use with all the classes
     * @return The succesfully loaded classes, or null on error.
     * @throws ClassNotFoundException
     */
    public Class<?>[] getClasses(String key, ConfigurationClassLoader loader)
	    throws ClassNotFoundException {
	return loader.loadClasses(key, this);
    }

    /**
     * Utility method to get a property as a File.
     * 
     * @param key
     *            the key of the property
     * @return null if key not contained, or file with the path as the valueif
     *         it is contained
     */
    public File getFile(String key) {
	if (!containsKey(key))
	    return null;
	else
	    return new File(getProperty(key));
    }

    /**
     * Utility method to get property parsed to a double.
     * 
     * @param key
     *            key to get value for
     * @return the double value if right format and key contained, null
     *         otherwise
     */
    public Double getDouble(String key) {
	if (!containsKey(key))
	    return null;
	else {
	    try {
		return Double.parseDouble(getProperty(key));
	    } catch (Exception e) {
		logger.error(StringUtil.getStackTrace(e));
		return null;
	    }
	}
    }

    /**
     * Utility method to get property parsed to a list of doubles.
     * 
     * @param key
     *            key to get values for
     * @return the double values if right format and key contained, null
     *         otherwise
     */
    public double[] getDoubles(String key) {
	if (!containsKey(key))
	    return null;
	else {
	    try {
		String[] strings = getStrings(key);
		double[] doubles = new double[strings.length];
		for (int i = 0; i < doubles.length; i++) {
		    try {
			doubles[i] = Double.parseDouble(strings[i]);
		    } catch (IllegalFormatException e) {
			return null;
		    }
		}
		return doubles;
	    } catch (Exception e) {
		logger.error(StringUtil.getStackTrace(e));
		return null;
	    }
	}
    }

    /**
     * Utility method to get property parsed to an int.
     * 
     * @param key
     *            key to get value for
     * @return the int value if right format and key contained, null otherwise
     */
    public Integer getInt(String key) {
	if (!containsKey(key))
	    return null;
	else {
	    try {
		return Integer.parseInt(getProperty(key));
	    } catch (Exception e) {
		logger.error(StringUtil.getStackTrace(e));
		return null;
	    }
	}
    }

    /**
     * Utility method to get property parsed to a list of ints.
     * 
     * @param key
     *            key to get values for
     * @return the int values if right format and key contained, null otherwise
     */
    public int[] getInts(String key) {
	if (!containsKey(key))
	    return null;
	else {
	    try {
		String[] strings = getStrings(key);
		int[] ints = new int[strings.length];
		for (int i = 0; i < ints.length; i++) {
		    try {
			ints[i] = Integer.parseInt(strings[i]);
		    } catch (IllegalFormatException e) {
			return null;
		    }
		}
		return ints;
	    } catch (Exception e) {
		logger.error(StringUtil.getStackTrace(e));
		return null;
	    }
	}
    }

    /**
     * Utility method to get property parsed to a long.
     * 
     * @param key
     *            key to get value for
     * @return the long value if right format and key contained, false otherwise
     */
    public Long getLong(String key) {
	if (!containsKey(key))
	    return null;
	else {
	    try {
		return Long.parseLong(getProperty(key));
	    } catch (Exception e) {
		logger.error(StringUtil.getStackTrace(e));
		return null;
	    }
	}
    }

    /**
     * Utility method to get property parsed to a list of longs.
     * 
     * @param key
     *            key to get values for
     * @return the long values if right format and key contained, null otherwise
     */
    public long[] getLongs(String key) {
	if (!containsKey(key))
	    return null;
	else {
	    try {
		String[] strings = getStrings(key);
		long[] longs = new long[strings.length];
		for (int i = 0; i < longs.length; i++) {
		    try {
			longs[i] = Long.parseLong(strings[i]);
		    } catch (IllegalFormatException e) {
			return null;
		    }
		}
		return longs;
	    } catch (Exception e) {
		logger.error(StringUtil.getStackTrace(e));
		return null;
	    }
	}
    }

    /**
     * If key is associated with a series of LIST_SEPERATOR seperated (used by
     * setStrings method), this retrieves an array of those Strings.
     * 
     * @param key
     *            the key to get strings for
     * @return an array of Strings, or null if the key is invalid
     */
    public String[] getStrings(String key) {
	if (!containsKey(key))
	    return null;
	return getProperty(key).split(LIST_SEPERATOR);
    }

    /**
     * Reloads the configuration from the currently associated propertyFile.
     * 
     * @return true if sucessful, false of file not set or that is an error
     */
    public boolean reloadConfiguration() {
	if (propertyFile != null && this.propertyFile.exists()) {
	    FileInputStream reader = null;
	    try {
		reader = new FileInputStream(propertyFile);

		if (isXML)
		    loadFromXML(reader);
		else
		    load(reader);
		return true;
	    } catch (InvalidPropertiesFormatException e) {

		logger.error(StringUtil.getStackTrace(e));
	    } catch (FileNotFoundException e) {

		logger.error(StringUtil.getStackTrace(e));
	    } catch (IOException e) {

		logger.error(StringUtil.getStackTrace(e));
	    } finally {
		if (reader != null)
		    try {
			reader.close();
		    } catch (IOException e) {
			logger.error(StringUtil.getStackTrace(e));
		    }
	    }
	}
	return false;
    }

    /**
     * Saves the configuration to its current file if the file exists and there
     * are no errors.
     * 
     * @return true if sucessful, false of file not set or that is an error
     */
    public boolean saveConfiguration() {
	return saveConfiguration(false);
    }

    /**
     * Saves the configuration to its current file, or create
     * 
     * @param create
     *            a boolean to indicate if the file should be created if it does
     *            not exist
     * @return true if sucessful, false of file not set or that is an error
     */
    public boolean saveConfiguration(boolean create) {
	if (propertyFile != null && this.propertyFile.exists()) {
	    FileOutputStream writer = null;
	    try {
		writer = new FileOutputStream(propertyFile);

		if (isXML)
		    storeToXML(
			    writer,
			    "Last saved at time "
				    + StringUtil.getFormattedTime());

		else
		    store(writer,
			    "Last saved at time "
				    + StringUtil.getFormattedTime());
		return true;
	    } catch (InvalidPropertiesFormatException e) {
		logger.error(StringUtil.getStackTrace(e));
	    } catch (FileNotFoundException e) {
		logger.error(StringUtil.getStackTrace(e));
	    } catch (IOException e) {
		logger.error(StringUtil.getStackTrace(e));
	    } finally {
		if (writer != null)
		    try {
			writer.close();
		    } catch (IOException e) {
			logger.error(StringUtil.getStackTrace(e));
		    }
	    }
	}
	return false;
    }

    /**
     * Associates the given key with a string representation of a boolean
     * 
     * @param key
     *            the key to store the boolean with
     * @param value
     *            the boolean value to convert to a string
     * @return The string value the key was associated to, or null if key is
     *         null.
     */
    public String setBoolean(String key, boolean value) {
	if (key != null) {
	    setProperty(key, Boolean.toString(value));
	    return getProperty(key);
	}
	return null;
    }

    /**
     * Associates the given key with a string representation of a list of
     * booleans
     * 
     * @param key
     *            the key to store the booleans with
     * @param value
     *            the boolean values to convert to a string
     * @return The string value the key was associated to, or null if key is
     *         null.
     */
    public String setBooleans(String key, Boolean... values) {
	if (key != null) {
	    return setObjectsAsStrings(key, (Object[]) values);
	}
	return null;
    }

    /**
     * Set the value of the name property to the name of a theClass implementing
     * the given interface interface. An exception is thrown if theClass does
     * not implement the interface.
     * 
     * @param key
     *            the key of the class type
     * @param theClass
     *            The class to save
     * @param superType
     *            the superinterface or superclass type that needs to be
     *            enforced
     * @throws WrongClassTypeException
     * @return the String value of what the key was associated to, or null if
     *         key is null.
     */
    public String setClass(String key, Class<?> theClass, Class<?> superType)
	    throws WrongClassTypeException {
	if (key != null)
	    if (superType.isAssignableFrom(theClass)) {
		setProperty(key, theClass.toString());
		return getProperty(key);
	    } else {
		throw new WrongClassTypeException("Tried saving " + theClass
			+ " as a class of type of " + superType + ".",
			superType);
	    }
	return null;
    }

    /**
     * Set the value of the name property to the name of a theClas.
     * 
     * @param key
     *            the key of the class type
     * @param theClass
     *            The class to save
     * @return the String value of what the key was associated to, or null if
     *         key is null.
     */
    public String setClass(String key, Class<?> theClass) {
	if (key != null) {
	    setProperty(key, theClass.getName());
	    return getProperty(key);
	}
	return null;
    }

    /**
     * Saves a list of classes to one key in the configuration. The classes are
     * stored by their getName() value and can be retrieved through getClasses.
     * 
     * @param key
     *            the key of the list of classes
     * @param classes
     *            The classes to save
     * @return the String value of what the key was associated to, or null if
     *         key is null.
     */
    public String setClasses(String key, Class<?>... classes) {
	String[] names = new String[classes.length];
	for (int i = 0; i < classes.length; i++)
	    names[i] = classes[i].getName();
	return setStrings(key, names);
    }

    /**
     * Sets the value associated with the key to the value if it is not already
     * set.
     * 
     * @param key
     *            the key
     * @param value
     *            the value
     * @return true if the value was set, false otherwise
     */
    public boolean setIfUnset(String key, String value) {
	if (key != null && !containsKey(key)) {
	    setProperty(key, value);
	    return true;
	}
	return false;
    }

    /**
     * Sets the key to the String value of the int.
     * 
     * @param key
     * @param value
     * @return the String value of the int stored, or null if key is null.
     */
    public String setInt(String key, int value) {
	if (key != null) {
	    setProperty(key, Integer.toString(value));
	    return getProperty(key);

	}
	return null;
    }

    /**
     * Sets the key to the String value of the this list of ints. Retrieved with
     * getInts.
     * 
     * @param key
     * @param value
     * @return the String value of the ints stored, or null if key is null.
     */
    public String setInts(String key, Integer... values) {
	if (key != null) {
	    return setObjectsAsStrings(key, (Object[]) values);
	}
	return null;
    }

    /**
     * Sets the key to the String value of the long.
     * 
     * @param key
     * @param value
     * @return the String value of the long stored, or null if key is null.
     */
    public String setLong(String key, long value) {
	if (key != null) {
	    setProperty(key, Long.toString(value));
	    return getProperty(key);
	}
	return null;
    }

    /**
     * Sets the key to the String value of the this list of longs. Retrieved
     * with getLongs.
     * 
     * @param key
     * @param value
     * @return the String value of the longs stored, or null if key is null.
     */
    public String setLongs(String key, Long... values) {
	if (key != null) {
	    return setObjectsAsStrings(key, (Object[]) values);
	}
	return null;
    }

    /**
     * Sets the key to the String value of the double.
     * 
     * @param key
     * @param value
     * @return the String value of the double stored, or null if key is null.
     */
    public String setDouble(String key, long value) {
	if (key != null) {
	    setProperty(key, Double.toString(value));
	    return getProperty(key);
	}
	return null;
    }

    /**
     * Sets the key to the String value of the this list of doubles. Retrieved
     * with getDoubles.
     * 
     * @param key
     * @param value
     * @return the String value of the doubles stored, or null if key is null.
     */
    public String setDoubles(String key, Double... values) {
	if (key != null) {
	    return setObjectsAsStrings(key, (Object[]) values);
	}
	return null;
    }

    /**
     * Associates the given strings with the key using the given seperator. The
     * strings are stored simply as a seperator-seperated string combination of
     * all the individual strings. The getStrings method can be used to retrieve
     * an array of these strings.
     * 
     * @param key
     *            the key
     * @param seperator
     *            the seperator String
     * @param values
     *            the Strings to associate the key
     * @return the String that the key is now associated with, or null if key is
     *         null.
     */
    public String setStrings(String key, String seperator, String... values) {
	return setObjectsAsStrings(key, seperator, (Object[]) values);
    }

    /**
     * Associates the given strings with the key using a default seperator
     * string. The strings are stored simply as a LIST_SEPERATOR-seperated
     * string combination of all the individual strings. The getStrings method
     * can be used to retrieve an array of these strings, but will work wrongly
     * if any of the values strings have LIST_SEPERATOR in them.
     * 
     * @param key
     *            the key
     * @param values
     *            the String to associate the key
     * @return the String that the key is now associated with, or null if key is
     *         null.
     */
    public String setStrings(String key, String... values) {
	return setObjectsAsStrings(key, LIST_SEPERATOR, (Object[]) values);
    }

    /**
     * Associates the given objects' toString() with the key using a default
     * seperator string. The toStrings are stored simply as a
     * LIST_SEPERATOR-seperated string combination of all the individual
     * strings. The getStrings method can be used to retrieve an array of these
     * strings, but will work wrongly if any of the values strings have
     * LIST_SEPERATOR in them.
     * 
     * @param key
     *            the key
     * @param values
     *            the String to associate the key
     * @return the String that the key is now associated with, or null if key is
     *         null.
     */
    public String setObjectsAsStrings(String key, Object... values) {
	return setObjectsAsStrings(key, LIST_SEPERATOR, values);
    }

    /**
     * Associates the given objects' toString() values with the key using the
     * given seperator. The toStrings are stored simply as a seperator-seperated
     * string combination of all the individual strings. The getStrings method
     * can be used to retrieve an array of these strings.
     * 
     * @param key
     *            the key
     * @param seperator
     *            the seperator String
     * @param values
     *            the Objects to associate the key
     * @return the String that the key is now associated with, or null if key is
     *         null.
     */
    public String setObjectsAsStrings(String key, String seperator,
	    Object... values) {
	if (key != null) {
	    StringBuilder builder = new StringBuilder();
	    for (Object obj : values) {
		builder.append(LIST_SEPERATOR + obj.toString());
	    }
	    builder.delete(0, LIST_SEPERATOR.length());
	    setProperty(key, builder.toString());
	    return getProperty(key);
	}
	return null;
    }

}
